/**
 * Project: d20Engine
 * Created: Aug 13, 2006 by bebopJMM
 *------------------------------------------------------------------------------
 * $Id$
 */
package org.rollinitiative.d20.entity;

import java.util.ArrayList;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.UUID;

import javax.xml.bind.annotation.*;
import javax.xml.namespace.QName;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.rollinitiative.d20.AdjustableValue;
import org.rollinitiative.d20.Adjustment;
import org.rollinitiative.d20.Adjustment.AdjustmentSource;
import org.rollinitiative.d20.encounter.combat.Attacks;
import org.rollinitiative.d20.encounter.combat.Defense;
import org.rollinitiative.d20.entity.HitPoints.LifeState;
import org.rollinitiative.d20.entity.Movement.EncumberanceType;
import org.rollinitiative.d20.entity.classes.CharacterClass;
import org.rollinitiative.d20.entity.races.Race;
import org.rollinitiative.d20.entity.races.RacialLevel;
import org.rollinitiative.d20.entity.races.RacialLevel.CombatSpec;
import org.rollinitiative.d20.entity.talents.Skill;
import org.rollinitiative.d20.entity.talents.SkillRules;
import org.rollinitiative.d20.entity.talents.SkillTraining;
import org.rollinitiative.d20.entity.talents.Talent;
import org.rollinitiative.d20.items.Inventory;
import org.rollinitiative.d20.items.ItemInstance;

/**
 * An actor is an entity in the game capable of performing independent actions.
 * 
 * @author bebopjmm
 * 
 */
@XmlType(name = "ActorType", namespace = "java:org.rollinitiative.d20.entity", propOrder = {
      "baseScores", "description", "characterLevels", "inventory" })
public abstract class Actor implements Comparable
{
   static final Log LOG = LogFactory.getLog(Actor.class);

   public static final String DEFAULT_NAME = "Nameless Actor";

   public enum Modifiers {
      AbilityMod;
   }

   public enum Values {
      Ability, Defense;
   }

   private final UUID uuid;

   private String name;

   private QName raceID;

   private Race race;

   Size currentSize;

   Alignment alignment;

   Description description;

   int level = 0;

   // --- Ability Information ---
   @XmlElement(name = "BaseAbilityScores", required = true)
   AbilityScores baseScores = new AbilityScores();

   Advancement characterLevels;

   EnumMap<Ability, AbilityValue> abilities;

   EnumMap<SavingThrow, AbilityListenerValue> saves;

   static SkillRules skillRules;

   @XmlTransient
   Hashtable<Skill, ManagedSkill> skillsTable;

   @XmlTransient
   HashMap<CharacterClass, Integer> classMap = new HashMap<CharacterClass, Integer>();

   ArrayList<Talent> talents;

   AbilityListenerValue initiativeMod;

   Attacks attackStats;

   Defense defenseStats;

   Movement movement;

   EncumberanceType currentEncumberance = EncumberanceType.LIGHT;

   HitPoints hp;

   @XmlElement(name = "Inventory", required = true, nillable = true)
   Inventory inventory;


   /**
    * Instantiates a new actor with DEFAULT_NAME. All abilities are initialized to
    * AbilityValue.DEFAULT_VALUE.
    * 
    * @since incr-0.1
    */
   public Actor()
   {
      this(Actor.DEFAULT_NAME);

   }


   /**
    * Instantiates a new actor with the provided name. All abilities are initialized to
    * AbilityValue.DEFAULT_VALUE.
    * 
    * @param name The full name of this Actor
    * @since incr-0.1
    */
   public Actor(String name)
   {
      this.uuid = UUID.randomUUID();
      this.name = name;
      this.raceID = new QName("NOT-SET");
      this.description = new Description();
      this.abilities = new EnumMap<Ability, AbilityValue>(Ability.class);
      for (Ability a : EnumSet.range(Ability.STR, Ability.CHA)) {
         this.abilities.put(a, new AbilityValue(AbilityValue.DEFAULT_VALUE, a));
      }
      this.characterLevels = new Advancement();

      // Initialize the saves
      this.saves = new EnumMap<SavingThrow, AbilityListenerValue>(SavingThrow.class);
      AbilityListenerValue fortSave = new AbilityListenerValue("FORT", 0, abilities
            .get(Ability.CON));
      this.saves.put(SavingThrow.FORTITUDE, fortSave);
      AbilityListenerValue reflSave = new AbilityListenerValue("REFL", 0, abilities
            .get(Ability.DEX));
      this.saves.put(SavingThrow.REFLEX, reflSave);
      AbilityListenerValue willSave = new AbilityListenerValue("WILL", 0, abilities
            .get(Ability.WIS));
      this.saves.put(SavingThrow.WILL, willSave);

      // initialize hit points.
      this.hp = new HitPoints(abilities.get(Ability.CON), true);

      // initialize with no known skills
      // this.skills_ = new Hashtable<Skill, AbilityListenerValue>();
      this.skillsTable = new Hashtable<Skill, ManagedSkill>();

      // initialize with no known talents
      this.talents = new ArrayList<Talent>();

      currentSize = Size.MEDIUM;

      this.description = new Description();

      initiativeMod = new AbilityListenerValue("INIT", 0, abilities.get(Ability.DEX));
      attackStats = new Attacks(currentSize, abilities.get(Ability.STR),
            abilities.get(Ability.DEX), abilities.get(Ability.STR));
      defenseStats = new Defense(currentSize, abilities.get(Ability.DEX));

      this.movement = new Movement(30, 20, 20);

      this.inventory = new Inventory();
   }


   public UUID getActorID()
   {
      return uuid;
   }


   /**
    * @return the level_
    */
   public int getLevel()
   {
      return level;
   }


   /**
    * @param level_ the level_ to set
    */
   @XmlTransient
   public void setLevel(int level)
   {
      this.level = level;
   }


   public HitPoints getHitPoints()
   {
      return hp;
   }


   /**
    * This method returns the current hit points for the actor.
    * 
    * @return current hit points for the actor.
    */
   public int getHP()
   {
      return hp.getCurrentHP();
   }


   /**
    * This method returns the maximum hit points for the actor.
    * 
    * @return maximum hit points for the actor.
    */
   public int getMaxHP()
   {
      return hp.getMaxHP();
   }


   /**
    * This method inflicts the designated amount of hit point damage to the actor.
    * 
    * @param hp amount of damage to inflict
    * @return LifeState following the damage.
    */
   public LifeState damage(int hp)
   {
      synchronized (this.hp) {
         return this.hp.damage(hp);
      }
   }


   /**
    * This method cures the designated amount of hit points to the actor.
    * 
    * @param hp amount of curing to apply
    * @return LifeState following the curing
    */
   public LifeState heal(int hp)
   {
      synchronized (this.hp) {
         return this.hp.heal(hp);
      }
   }


   /**
    * The actor's initiative modifier is a value derived from dexterity bonus and other modifiers.
    * 
    * @return the actor's modifier to initiative rolls.
    * @since incr-0.1
    */
   public AbilityListenerValue getInitiativeMod()
   {
      return initiativeMod;
   }


   /**
    * This method revises the baseValue of the designated ability.
    * 
    * @param ability Ability to be modified
    * @param baseValue new baseValue for the designated ability
    */
   public void setAbilityBaseValue(Ability ability, int baseValue)
   {
      LOG.debug("Setting " + ability + " baseValue to " + baseValue);
      AbilityValue abilVal = abilities.get(ability);
      abilVal.setBase(baseValue);
   }


   /**
    * Accessor for the Actor's name.
    * 
    * @return Returns the name.
    * @since incr-0.1
    */
   public String getName()
   {
      return name;
   }


   /**
    * Allows specifying the name of this Actor.
    * 
    * @param name The name to set.
    * @since incr-0.1
    */
   @XmlAttribute(required = true)
   public void setName(String name)
   {
      this.name = name;
   }


   public QName getRaceID()
   {
      return raceID;
   }


   /**
    * @param raceID the raceID to set
    */
   @XmlAttribute(required = true)
   public void setRaceID(QName raceID)
   {
      this.raceID = raceID;
   }
   
   public Race getRace()
   {
      return this.race;
   }


   /**
    * Accessor for the Actor's current Size.
    * 
    * @return the actor's currentSize
    * @since incr-0.1
    */
   public Size getCurrentSize()
   {
      return currentSize;
   }


   /**
    * Allows specifying the Size of an Actor
    * 
    * @param currentSize new size for the actor
    * @since incr-0.1
    */
   @XmlTransient
   public void setCurrentSize(Size currentSize)
   {
      this.currentSize = currentSize;
      attackStats.setSize(this.currentSize);
      defenseStats.setSize(this.currentSize);
   }


   /**
    * @return the alignment
    */
   public Alignment getAlignment()
   {
      return alignment;
   }


   /**
    * @param alignment the alignment to set
    */
   @XmlAttribute(required = true)
   public void setAlignment(Alignment alignment)
   {
      this.alignment = alignment;
   }


   /**
    * @return the description
    */
   public Description getDescription()
   {
      return description;
   }


   /**
    * @param description the description to set
    */
   @XmlElement(name = "Description", required = true)
   public void setDescription(Description description)
   {
      this.description = description;
   }


   /**
    * @return the characterLevels
    */
   public Advancement getCharacterLevels()
   {
      return characterLevels;
   }


   /**
    * @param characterLevels the characterLevels to set
    */
   @XmlElement(name = "Advancement", required = true)
   public void setCharacterLevels(Advancement characterLevels)
   {
      this.characterLevels = characterLevels;
      this.level = characterLevels.levels.size();
   }


   public AdjustableValue getAbility(Ability ability)
   {
      return abilities.get(ability);
   }


   public AdjustableValue getSave(SavingThrow save)
   {
      return saves.get(save);
   }


    public AdjustableValue getSkillValue(Skill skill)
   {
      ManagedSkill mSkill = skillsTable.get(skill);
      if (mSkill == null) {
         LOG.warn("Requested Skill (" + skill.getId() + ") is not known by this actor: " + name);
         return null;
      }
      else {
         return mSkill.skillValue;
      }
   }

    public Skill[] getSkills()
    {
       Set<Skill> skillSet = skillsTable.keySet();
       return skillSet.toArray(new Skill[skillSet.size()]);
    }

   public boolean isClassSkill(Skill skill)
   {
      Set<CharacterClass> charClasses = classMap.keySet();
      for (CharacterClass characterClass : charClasses) {
         if (characterClass.isClassSkill(skill.getId())) {
            return true;
         }
      }
      return false;
   }


   /**
    * This method returns true if the actor has the specified skill in their repertoire of known
    * skills.
    * 
    * @param skill Skill to check for
    * @return true if the actor has the specified skill in their repertoire of known skills,
    *         otherwise false.
    */
   public boolean hasSkill(Skill skill)
   {
      return skillsTable.containsKey(skill);
   }


   public void advanceSkill(Skill skill, int points)
   {
      LOG.debug("Advancing ranks in " + skill.getName() + " by " + points);
      if (!skillsTable.containsKey(skill)) {
         LOG.info("Initializing new skill before training: " + skill.getName());
         initSkill(skill);
      }
      ManagedSkill managedSkill = skillsTable.get(skill);
      managedSkill.addRanks(points);

      // TODO Check if class skill bonus has been applied.
      if (isClassSkill(skill) && (managedSkill.classSkillAdjustment == null)) {
         LOG.info("Adding class skill bonus to " + skill.getName());
         managedSkill.setClassSkillAdjustment(skillRules.getClassSkillBonus());
      }
   }


   public Talent[] getTalents()
   {
      return talents.toArray(new Talent[talents.size()]);
   }


   public void addTalent(Talent newTalent)
   {
      if (talents.contains(newTalent)) {
         LOG.warn("Ignoring attempt to add talent (" + newTalent.getName()
               + ") that already exists for actor: " + this.name);
         return;
      }
      else {
         LOG.debug("Adding new talent (" + newTalent.getName() + ") to actor: " + this.name);
         talents.add(newTalent);
      }
   }


   public int getCurrentSpeed()
   {
      return movement.getSpeed(currentEncumberance);
   }


   public Attacks getAttacks()
   {
      return this.attackStats;
   }


   public Defense getDefense()
   {
      return this.defenseStats;
   }


   public void setMaxBaseAttackBonus(int maxBonus)
   {
      this.attackStats.setBaseAttackBonus(maxBonus);
   }


   public ItemInstance[] getCurrentInventory()
   {
      return inventory.getContents();
   }


   /*
    * (non-Javadoc)
    * 
    * @see java.lang.Comparable#compareTo(java.lang.Object)
    */
   public int compareTo(Object object)
   {
      if (object instanceof Actor) {
         return uuid.compareTo(((Actor) object).uuid);
      }
      return 0;
   }


   /*
    * (non-Javadoc)
    * 
    * @see java.lang.Object#equals(java.lang.Object)
    */
   @Override
   public boolean equals(Object obj)
   {
      if (obj instanceof Actor) {
         Actor a = (Actor) obj;
         return a.uuid.equals(this.uuid);
      }
      return super.equals(obj);
   }


   void setRace(Race race)
   {
      this.race = race;
      this.raceID = race.getId();
      setCurrentSize(race.getSize());

      // Configure Movement
      for (Movement.EncumberanceType encumberance : EnumSet.range(EncumberanceType.LIGHT,
            EncumberanceType.HEAVY)) {
         int speed = race.getMovement().getSpeed(encumberance);
         setBaseMovement(encumberance, speed);
         LOG.debug("Base movement for encumberance: " + encumberance.toString() + " = " + speed);
      }

      // Apply racial modifiers to abilities
      int nLevels = this.level;
      if (race.getAdvancement().levels.size() < nLevels) {
         nLevels = race.getAdvancement().levels.size();
         LOG.debug("Total racial levels based on race, = " + nLevels);
      }
      RacialLevel level;
      for (int i = 0; i < nLevels; i++) {

         level = race.getAdvancement().levels.get(i);

         // Apply racial modifiers to abilities
         AbilityModifier[] abilMods = level.getAbilityModifiers();
         for (AbilityModifier abilityModifier : abilMods) {
            AdjustableValue abilVal = abilities.get(abilityModifier.getAbility());
            Adjustment abilAdj = new Adjustment(AdjustmentSource.INHERENT, abilityModifier
                  .getModifier(), abilityModifier.getId());
            abilVal.addAdjustment(abilAdj);
         }

         // Apply racial modifiers to skills
         SkillTraining[] skillMods = level.getSkillBonuses();
         Hashtable<QName, Adjustment> adjustmentTable = new Hashtable<QName, Adjustment>();
         Adjustment racialSkillBonus = null;
         for (SkillTraining trainingType : skillMods) {
            if (LOG.isDebugEnabled()) {
               LOG.debug("At level " + (i + 1) + ", " + raceID.getLocalPart()
                     + " has skill bonus: " + trainingType.getSkillID().getLocalPart() + " = "
                     + trainingType.getPoints());
            }
            if (adjustmentTable.containsKey(trainingType.getSkillID())) {
               racialSkillBonus = adjustmentTable.get(trainingType.getSkillID());
               int value = racialSkillBonus.getValue();
               value += trainingType.getPoints();
               racialSkillBonus.setValue(value);
            }
            else {
               racialSkillBonus = new Adjustment(AdjustmentSource.INHERENT, trainingType
                     .getPoints(), raceID);
               adjustmentTable.put(trainingType.getSkillID(), racialSkillBonus);
            }

         }
      }
      // Apply racial combat modifiers of max Level
      level = race.getAdvancement().levels.get(nLevels - 1);
      CombatSpec combatMods = level.getCombatMods();
      if (combatMods.getNaturalArmor() > 0) {
         // Apply racial natural armor bonus
         Adjustment naturalArmor = new Adjustment(AdjustmentSource.INHERENT, combatMods
               .getNaturalArmor(), new QName("java:org.rollinitiative.d20.entity", "racial.combatMods"));
         defenseStats.addNaturalArmor(naturalArmor);
      }
      // Racial adjustment to BAB
      attackStats.modifyBaseAttackBonus(combatMods.getRacialBAB());
   }


   void setBaseMovement(EncumberanceType encumberance, int speed)
   {
      AdjustableValue move = movement.getSpeedValue(encumberance);
      move.setBase(speed);
   }


   protected static void setSkillRules(SkillRules rules)
   {
      Actor.skillRules = rules;
   }


   protected void initAbilities()
   {
      LOG.debug("Initializing STR to " + this.baseScores.str);
      setAbilityBaseValue(Ability.STR, this.baseScores.str);
      setAbilityBaseValue(Ability.DEX, this.baseScores.dex);
      setAbilityBaseValue(Ability.CON, this.baseScores.con);
      setAbilityBaseValue(Ability.INT, this.baseScores.int_);
      setAbilityBaseValue(Ability.WIS, this.baseScores.wis);
      setAbilityBaseValue(Ability.CHA, this.baseScores.cha);
   }


   protected void initSkill(Skill skill)
   {
      ManagedSkill newSkill = new ManagedSkill(skill.getId(), abilities.get(skill.getKeyAbility()));
      synchronized (skillsTable) {
         if (skillsTable.containsKey(skill)) {
            LOG.warn("Ignoring attempt to initialize skill already known: " + skill.getName());
            return;
         }
         skillsTable.put(skill, newSkill);
      }
   }


   void levelUpHP(int increaseHP)
   {
      hp.advanceLevel(increaseHP);
   }


   AdjustableValue getAdjustableValue(Values selection, StringTokenizer descriptor)
         throws IllegalArgumentException
   {
      // i.e. Ability.WIS or Defense.AC_Reg
      switch (selection) {
      case Defense:
         Defense.AC_Type acType = Defense.AC_Type.valueOf(descriptor.nextToken());
         return defenseStats.getArmorClass(acType);

      case Ability:
         Ability ability = Ability.valueOf(descriptor.nextToken());
         return abilities.get(ability);

      default:
         return null;
      }
   }


   Adjustment getAdjustment(Modifiers selection, StringTokenizer descriptor)
         throws IllegalArgumentException
   {
      String token = null;
      // i.e. Ability.WIS
      switch (selection) {
      case AbilityMod:
         token = descriptor.nextToken();
         LOG.debug("Looking up ability with token: " + token);
         Ability ability = Ability.valueOf(token);
         return abilities.get(ability).getModifier();

      default:
         break;
      }
      return null;
   }

   @XmlType(name = "AbilityScoresType", namespace = "java:org.rollinitiative.d20.entity")
   public static class AbilityScores
   {
      @XmlElement(name = "STR", required = true)
      int str = 0;

      @XmlElement(name = "DEX", required = true)
      int dex = 0;

      @XmlElement(name = "CON", required = true)
      int con = 0;

      @XmlElement(name = "INT", required = true)
      int int_ = 0;

      @XmlElement(name = "WIS", required = true)
      int wis = 0;

      @XmlElement(name = "CHA", required = true)
      int cha = 1;

   }

   @XmlTransient
   class ManagedSkill
   {
      public QName skillID;

      public AbilityListenerValue skillValue;

      public Adjustment ranksAdjustment;

      public Adjustment classSkillAdjustment = null;


      public ManagedSkill(QName skillID, AbilityValue keyAbility)
      {
         this.skillID = skillID;
         skillValue = new AbilityListenerValue(skillID.getLocalPart(), 0, keyAbility);
         ranksAdjustment = new Adjustment(AdjustmentSource.INHERENT, 0, new QName("java:org.rollinitiative.d20.entity","skillRanks"));
         skillValue.addAdjustment(ranksAdjustment);
         LOG.debug("Ability adjusted score for initialized skill, " + this.skillID.getLocalPart()
               + " = " + skillValue.getCurrent());
      }


      public boolean hasClassAdjustment()
      {
         return classSkillAdjustment != null;
      }


      public void setClassSkillAdjustment(int bonus)
      {
         classSkillAdjustment = new Adjustment(AdjustmentSource.INHERENT, bonus, new QName("java:org.rollinitiative.d20.entity","class"));
         skillValue.addAdjustment(classSkillAdjustment);
      }


      public void addRanks(int points)
      {
         int ranks = ranksAdjustment.getValue() + points;
         ranksAdjustment.setValue(ranks);
      }
   }

   @XmlType(name = "CharacterLevelListType", namespace = "java:org.rollinitiative.d20.entity")
   public static class Advancement
   {
      @XmlElement(name = "LevelEntry", required = true)
      ArrayList<CharacterLevel> levels = new ArrayList<CharacterLevel>();
   }
}
